-
-
Notifications
You must be signed in to change notification settings - Fork 33
Compiled property path function #178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Compiled property path function #178
Conversation
Thank you so much for these changes, @DanielTrommel. I tried some benchmarking on these changes, and they are indeed fast. However, I'm a bit concerned because these chagnes are effecting core functionality therfore it would be good to release these in a preview release first. So, let's hold off on these changes for the v1.4 preview release. Thanks again! |
@w-ahmad sounds like a safe plan. Happy to fix any bug reports once it gets merged; just tag me. |
…e reflection approach Signed-off-by: Daniël Trommel <[email protected]>
fc10df0
to
d932f8c
Compare
PS: noticed that my commits were commited under my (work) email account, which is not handy. I recommited the changes (squashed the two commits into one), and force-pushed the changes. Nothing else changed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR refactors the property path evaluation mechanism to use compiled lambda expressions instead of reflection-based property access. The change replaces the existing reflection-based GetValue
methods with a compiled function approach that builds expression trees and compiles them for faster runtime evaluation.
Key changes:
- Replaced reflection-based property path evaluation with compiled lambda expressions
- Eliminated the need to maintain synchronized
GetValue
methods - Simplified the caching mechanism in
TableViewBoundColumn
Reviewed Changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
File | Description |
---|---|
src/Extensions/ObjectExtensions.cs | Replaced reflection-based GetValue methods with GetFuncCompiledPropertyPath that builds and compiles expression trees |
src/Columns/TableViewBoundColumn.cs | Updated to use the new compiled property path function, removing type-based caching logic |
Signed-off-by: Daniël Trommel <[email protected]>
@w-ahmad just processed the comments! |
Thanks @DanielTrommel! |
…perty-path-function # Conflicts: # src/Extensions/ObjectExtensions.cs
@w-ahmad when are you planning to merge the UnitTest branch? Happy to integrate those unit tests, and update them to accomodate this new approach, so you can see it works for those tests. |
…ew compiled function approach. Note: the support for Dictionary Signed-off-by: Daniël Trommel <[email protected]>
It's merged! |
@w-ahmad just pushed the changes; few notes:
|
…erty-path-function
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hey @DanielTrommel, thanks for the updates. I ran two more tests, and they failed. These scenarios involve a compiled function that fails when invoked with a different data item. Please try these yourself. This fix is necessary since, in real-world usage, there's no guarantee we'll always get the same data items.
[TestMethod]
public void GetFuncCompiledPropertyPath_ShouldReturnNull_ForSimpleNestedProperty()
{
var testItem = new TestItem { SubItems = [new() { SubSubItems = [new() { Name = "NestedValue" }] }] };
var func = testItem.GetFuncCompiledPropertyPath("SubItems[0].SubSubItems[0].Name");
Assert.IsNotNull(func);
var result = func(testItem);
Assert.AreEqual("NestedValue", result);
testItem = new TestItem { SubItems = [new() { SubSubItems = null! }] };
result = func(testItem);
Assert.IsNull(result);
}
[TestMethod]
public void GetFuncCompiledPropertyPath_ShouldReturnNull_ForInvalidIndexer2()
{
var testItem = new TestItem { Dictionary2 = new() { { 1, "value1" } } };
var func = testItem.GetFuncCompiledPropertyPath("Dictionary2[1]");
Assert.IsNotNull(func);
var result = func(testItem);
Assert.AreEqual("value1", result);
testItem = new TestItem { Dictionary2 = new() { { 2, "value2" } } };
result = func(testItem);
Assert.IsNull(result);
}
Signed-off-by: Daniël Trommel <[email protected]>
Signed-off-by: Daniël Trommel <[email protected]>
Signed-off-by: Daniël Trommel <[email protected]>
…ression/Lambda code with DebugView Signed-off-by: Daniël Trommel <[email protected]> (cherry picked from commit 5c0ccd2b9be1866427b74c91d752f8fefa4b88b4)
…soon as one step yields null, to guard against non-complete property paths for some instances Signed-off-by: Daniël Trommel <[email protected]>
…tests for all possible cases Signed-off-by: Daniël Trommel <[email protected]>
@w-ahmad thanks for those tests. Based off this, I did a refactor, to add support for these, and various other things:
Added various test cases to support the code (in the next commits) |
Signed-off-by: Daniël Trommel <[email protected]>
Signed-off-by: Daniël Trommel <[email protected]>
…nnot be bound-checked, and do not have a TryGetValue construct like with dictionary. We could add code to search for such a method, but there is not guarentee that is the method we should use... Signed-off-by: Daniël Trommel <[email protected]>
- An explicit conversion is not always needed when an implicit conversion would take care of it - The in-loop compilation (used to determine the need for conversion) is not needed for the last match (Typically, Binding path is just accessing one property, and thus the inloop compilation will not be used, saving some time) Signed-off-by: Daniël Trommel <[email protected]>
…nement of the conversion logic
@w-ahmad ok, it feels quite complete now. Quite some code added, but also more supported scenario's. One thing that is still in the back of my mind, is that both the current It feels like it would be better to do the derivation Type-based, and take into account that the rows could be a mixture of instances of different types..? (would be good to test how 'flexible' the binding path in WinUI is to see what could be provided as data...) And also, one would need to take into consideration that due to subclassing, the binding path might yield different type-paths in effect, with (depending on the instance) might need different PropertyInfo's along the way, for the same part in the binding path... |
Thank you so much for the thoughtful improvements and for covering more scenarios. I really appreciate the effort put in and adding more unit tests to ensure everything works smoothly across different cases. Apologies for my late response. I was testing a different approach where we could use the binding itself to elevate the property values, instead of relying on reflection or compiled lambda expressions. The idea behind this is that our current implementation does not cover all Binding features. For example, we are not considering the fallback properties, or cases where Source or ElementName is specified in the Binding, or situations where users have their own Binding implementations. By leveraging the binding mechanism, we could potentially resolve all these issues. I know this approach is not as performant as reflection or compiled lambda expressions, but after testing with 100k items, I managed to find a method that takes less time than my initial tests from the early days of TableView. It takes less than a second, and that is acceptable if it allows us to handle all edge cases reliably. |
@w-ahmad no worries, always good to choose the better approach! A while back, I did look into whether the Binding resolving mechanism was exposed, but thought it was not. |
@w-ahmad While working on the other small PR (the one to support string-based indexers), I figured the code to build that
PropertyInfo
list from the Binding's PropertyPath can be implemented completely different, to make it easier and faster (for evaluation):Pro's of this approach:
GetValue(..)
methods that needed to be kept in sync. Now there is only one place where the logic resides to interpret the PropertyPathOnly downside is that (most likely) the initial building + compiling might be more costly, although I did not experience any difference on my implementation project(s). Typically, the PropertyPaths are rather small.
Tested this with my own tables and data, and tested it against your TableView sample app (where I filed a seemingly unrelated bug)